package com.spzx.channel.service.impl;

import com.spzx.channel.service.LockService;
import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;

import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class LockServiceImpl implements LockService {
    @Resource
    StringRedisTemplate stringRedisTemplate;

    //6.redis分布式锁v5版本：基于v4版本解决业务时长超过锁的国企时间，锁失效，其他线程可以获取到锁
    //解决：可以通过当前服务中的一个子线程，判断如果业务为执行完成 锁即将过期时，给锁续期 更新过期时间
    //   watchdog线程：看门狗线程！- 如何判断获取到锁的业务未执行结束？ 线程获取到锁设置了自己的唯一标志uuid，当线程正常结束时会删除key和uuid，其它线程获取到锁时uuid一定不同
    //  业务： 获取到锁的线程，同时给它开启一个子线程, 子线程每过一段时间(锁过期时间的2/3)去判断一次 如果锁的值还是uuid将锁过期时间更新，如果不是uuid 子线程结束
    // 问题：不支持重入性！

    @Override
    public void incr() {
        //==进入方法时，通过redis的setnx尝试获取锁  设置5s的过期时间  设置uuid
        String uuid = UUID.randomUUID().toString().replace("-", "");
        int timeout = 10;
        String lockKey = "channel:lock:num";
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", timeout, TimeUnit.SECONDS);
        if (flag) {
            //获取锁成功：执行业务
            //开启看门狗线程 对锁进行自动续期
            renewLock(lockKey,uuid,timeout);

            //获取redis中缓存的num
            String numStr = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isEmpty(numStr)) {
                //第一次，可以初始化num的值
                numStr = "0";
            }
            //执行num+1操作
            int num = Integer.parseInt(numStr);
            num++;
            int i = 1/0;
            //将+1后的结果设置到redis中
            stringRedisTemplate.opsForValue().set("num", num + "");
            //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
            // 原子性问题： 判断锁是不是自己的 和删除锁的逻辑 没有原子性
            //  解决: 需要保证 判断 和删除的逻辑具有原子性  可以利用redis的原子性 将一组redis操作和逻辑交给redis
            //       redis一次接收到的多个指令不会被其它的命令插队
        /*    if(uuid.equals(stringRedisTemplate.opsForValue().get("channel:lock:num"))){
                //s释放使用完的锁
                stringRedisTemplate.delete("channel:lock:num");
            }
*/
            String releaseLockScript = "local uuid = redis.call('get' , KEYS[1])   if( ARGV[1] == uuid) then return redis.call('del' , KEYS[1]) else return 0 end";

            String lockKey = "channel:lock:num";
            stringRedisTemplate.execute(new DefaultRedisScript<>(releaseLockScript, Long.class), Arrays.asList(lockKey), uuid);

        } else {
            //获取锁失败，再次尝试获取锁(递归调用
            //频繁调用可能会导致栈内存溢出：线程保存未执行完的方法信息时，会通过站栈结构(stack)存储，默认空间是1m
            try {
                Thread.sleep(100 + new Random().nextInt(100));
                incr();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
//锁的自动续期
    private void renewLock(String lockKey, String uuid, int timeout) {

        //使用子线程
        new Thread(() -> {
            try {
                //第一次判断是否续期，让子线程休眠 timeout*2/3秒在执行
                TimeUnit.SECONDS.sleep(timeout * 2 / 3);

                //2、判断是否要续期
                // 到redis中获取lockKey的值 如果 和uuid相等 更新锁的过期时间
                // 更新成功返回1  其它情况返回0表示失败(watchdog线程可以结束了)
                String script = "local uuid = redis.call('get' , KEYS[1]) " +
                        " if uuid == ARGV[1] then return redis.call('expire' , KEYS[1] , ARGV[2]) " +
                        " else return 0 end";
                //如果执行更新过期时间的命令返回成功 休眠后 再次执行
                //如果执行更新过期时间的命令失败，结束循环   watchdog线程正常结束

                while(StringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockKey), uuid, timeout + "")){
                    //execute接收的参数只能是字符串类型
                    TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                }
                //如果不等，表示当前子线程 要监听的锁业务已经结束 已经删除了 看门狗线程可以结束了.
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }


        }).start();

    }

    //5.redis分布式锁v4版本：基于v3版本解决删除锁的原子性问题   因为删除锁时，判断是不是自己的锁和删除锁的逻辑 不具有原子性，不是线程安全的，解决方案是：通过LUA脚本 将redis的多个操作和业务逻辑封装交给redis原子性操作执行
    /*@Override
    public void incr() {
        //==进入方法时，通过redis的setnx尝试获取锁  设置5s的过期时间  设置uuid
        String uuid = UUID.randomUUID().toString().replace("-", "");
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num", "1", 5, TimeUnit.SECONDS);
        if (flag) {
            //获取锁成功：执行业务
            //获取redis中缓存的num
            String numStr = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isEmpty(numStr)) {
                //第一次，可以初始化num的值
                numStr = "0";
            }
            //执行num+1操作
            int num = Integer.parseInt(numStr);
            num++;
            int i = 1/0;
            //将+1后的结果设置到redis中
            stringRedisTemplate.opsForValue().set("num", num + "");
            //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
            // 原子性问题： 判断锁是不是自己的 和删除锁的逻辑 没有原子性
            //  解决: 需要保证 判断 和删除的逻辑具有原子性  可以利用redis的原子性 将一组redis操作和逻辑交给redis
            //       redis一次接收到的多个指令不会被其它的命令插队
        *//*    if(uuid.equals(stringRedisTemplate.opsForValue().get("channel:lock:num"))){
                //s释放使用完的锁
                stringRedisTemplate.delete("channel:lock:num");
            }
*//*
            String releaseLockScript = "local uuid = redis.call('get' , KEYS[1])   if( ARGV[1] == uuid) then return redis.call('del' , KEYS[1]) else return 0 end";

            String lockKey = "channel:lock:num";
            stringRedisTemplate.execute(new DefaultRedisScript<>(releaseLockScript, Long.class), Arrays.asList(lockKey), uuid);

        } else {
            //获取锁失败，再次尝试获取锁(递归调用
            //频繁调用可能会导致栈内存溢出：线程保存未执行完的方法信息时，会通过站栈结构(stack)存储，默认空间是1m
            try {
                Thread.sleep(100 + new Random().nextInt(100));
                incr();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }*/

//4.redis分布式锁v3版本：基于v2版本，j解决误删除问题，给锁设置唯一的标志uuid，给每一个线程设置自己的唯一的值，删除锁时判断是否是自己的锁，是自己的删除，不是自己的不删除
    /*@Override
    public void incr() {
        //==进入方法时，通过redis的setnx尝试获取锁  设置5s的过期时间  设置uuid
        String uuid = UUID.randomUUID().toString().replace("-", "");
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num", "1", 5, TimeUnit.SECONDS);
        if (flag) {
            //获取锁成功：执行业务
            //获取redis中缓存的num
            String numStr = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isEmpty(numStr)) {
                //第一次，可以初始化num的值
                numStr = "0";
            }
            //执行num+1操作
            int num = Integer.parseInt(numStr);
            num++;
            int i = 1/0;
            //将+1后的结果设置到redis中
            stringRedisTemplate.opsForValue().set("num", num + "");
            //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
            // 原子性问题： 判断锁是不是自己的 和删除锁的逻辑 没有原子性
            //  解决: 需要保证 判断 和删除的逻辑具有原子性  可以利用redis的原子性 将一组redis操作和逻辑交给redis
            //       redis一次接收到的多个指令不会被其它的命令插队
            if(uuid.equals(stringRedisTemplate.opsForValue().get("channel:lock:num"))){
                //s释放使用完的锁
                stringRedisTemplate.delete("channel:lock:num");
            }

        } else {
            //获取锁失败，再次尝试获取锁(递归调用
            //频繁调用可能会导致栈内存溢出：线程保存未执行完的方法信息时，会通过站栈结构(stack)存储，默认空间是1m
            try {
                Thread.sleep(100 + new Random().nextInt(100));
                incr();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }*/

/*//3.redis分布式锁v2版本：基于v1版本可以给锁设置过期时间，但是会存在误删除的问题
    //比如当线程1获取到锁，执行业务，但是业务时间过长，超过了锁的过期时间，线程2在线程1锁过期的时候获取到了锁，执行自己的业务，线程1完成任务时，需要释放锁：执行删除锁的操作，此时线程3又可以获取到锁，这样就导致锁失效：因为同时两个线程执行
    @Override
    public void incr() {
        //==进入方法时，通过redis的setnx尝试获取锁  设置5s的过期时间
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num", "1", 5, TimeUnit.SECONDS);
        if (flag) {
            //获取锁成功：执行业务
            //获取redis中缓存的num
            String numStr = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isEmpty(numStr)) {
                //第一次，可以初始化num的值
                numStr = "0";
            }
            //执行num+1操作
            int num = Integer.parseInt(numStr);
            num++;
            int i = 1/0;
            //将+1后的结果设置到redis中
            stringRedisTemplate.opsForValue().set("num", num + "");
            //s释放使用完的锁
            stringRedisTemplate.delete("channel:lock:num");
        } else {
            //获取锁失败，再次尝试获取锁(递归调用
            //频繁调用可能会导致栈内存溢出：线程保存未执行完的方法信息时，会通过站栈结构(stack)存储，默认空间是1m
            try {
                Thread.sleep(100 + new Random().nextInt(100));
                incr();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }*/

   /* //2.redis分布式锁v1版本：通过redis的setnx尝试获取锁
    //有缺陷：当业务出现异常时，服务器宕机会导致锁 没有正常释放，导致死锁
    @Override
    public void incr() {
        //==进入方法时，通过redis的setnx尝试获取锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num", "1");
        if (flag) {
            //获取锁成功：执行业务
            //获取redis中缓存的num
            String numStr = stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isEmpty(numStr)) {
                //第一次，可以初始化num的值
                numStr = "0";
            }
            //执行num+1操作
            int num = Integer.parseInt(numStr);
            num++;
            //将+1后的结果设置到redis中
            stringRedisTemplate.opsForValue().set("num", num + "");
        } else {
            try {
                Thread.sleep(100 + new Random().nextInt(100));
                incr();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }*/

    //1.本地锁完成
   /* @Override
    public synchronized void incr() {
        //获取redis中缓存的num
        String numStr = stringRedisTemplate.opsForValue().get("num");
        if(StringUtils.isEmpty(numStr)){
            //第一次，可以初始化num的值
            numStr = "0";
        }
        //执行num+1操作
        int num = Integer.parseInt(numStr);
        num++;
        //将+1后的结果设置到redis中
        stringRedisTemplate.opsForValue().set("num",num+"");
    }*/

}
